# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import abc
import concurrent.futures
import functools
import json
import logging
import math
import asyncio

import requests
import tornado.httpclient
import google.protobuf.json_format as json_format

from typing import List, Optional, Union
from tornado import gen

import coin.proto.coin_market_enums_pb2 as coin_enums_pb2
import coin.proto.coin_order_enums_pb2 as coin_order_pb2

from coin.base.config import Config
from coin.base.timestamp import get_timestamp
from coin.exchange.base.kr_rest.currency import NativeCurrency
from coin.exchange.base.kr_rest.futures_product import NativeFuturesProduct
from coin.exchange.base.kr_rest.product import NativeProduct
from coin.exchange.base.order_fill_util import FillManager
from coin.exchange.base.order_gateway_logger import gen_og_log_request
from coin.exchange.base.order_id_util import OrderIdGenerator
from coin.exchange.base.order_manager import OrderManager
from coin.exchange.base.simple_order_gateway_log import SimpleOrderGatewayLogProvider
from coin.exchange.kr_rest.product_holders import get_holder_from_product
from coin.proto.coin_order_gateway_pb2 import OrderGatewayConfig, OrderEvent
from coin.strategy.hk_hard_arb.og_util import print_working_orders
from coin.exchange.base.fill_checker import FillChecker
from coin.proto.coin_query_pb2 import ProductFillElement
from coin.support.telemetry.latency_recorder import LifeOfSignal
from coin.exchange.base.rest.task_scheduler import Scheduler
from coin.exchange.base.fill_checker_order_info import (
    BaseBalanceOrderGatewayInfo,
    BasePositionOrderGatewayInfo,
)
from coin.exchange.base.order_gateway_ready import (
    PositionOrderGatewayReady,
    SpotOrderGatewayReady,
    SpotLPOrderGatewayReady,
)
from coin.exchange.base.order_gateway_log_builder import (
    to_proto_order_side,
    to_proto_order_type,
)
from coin.exchange.base.order_gateway import (
    Order,
    OrderGatewayBase,
    OrderGatewayStatus,
    OrderSide,
    OrderType,
)
from coin.exchange.base.order_gateway_util import (
    PrivateExchangeMessage,
    PrivateExchangeMessageEncoder,
    convert_to_requests_response,
    create_order_internal,
    pretty_json,
)

# Get rid of lint warning!
to_proto_order_side = to_proto_order_side
to_proto_order_type = to_proto_order_type
BaseBalanceOrderGatewayInfo = BaseBalanceOrderGatewayInfo
BasePositionOrderGatewayInfo = BasePositionOrderGatewayInfo


# Return a decorator that marks a handler with the msg type it should handle.
def exchange_message_handler(msg_type):
  def decorator(handler):
    handler.__coin_og_msg_type__ = msg_type
    return handler

  return decorator


class SimpleOrderFormatter(object):
  def floor_price(self, price):
    return str(price)

  def ceil_price(self, price):
    return str(price)

  def floor_qty(self, qty):
    return str(qty)

  def ceil_qty(self, qty):
    return str(qty)

  def format_price(self, price):
    return str(price)

  def format_qty(self, qty):
    return str(qty)


class RestOrderGateway(OrderGatewayBase, metaclass=abc.ABCMeta):
  def __init__(self,
               currencies: List[NativeCurrency],
               products: List[Union[NativeProduct, NativeFuturesProduct]],
               config: Config,
               og_config=None,
               logger=None,
               latency_recorder=None):
    super().__init__()

    # config
    self._config = config  # key
    self._og_config = og_config or OrderGatewayConfig()  # OG parameters

    # orders
    self._order_manager = OrderManager()
    self._order_id_generator = OrderIdGenerator()

    self._ioloop = None
    self._flow_sub = None
    self._scheduler = None

    self._og_info = self.OGInfoType(currencies, products)
    self._fill_checker = None
    self._processed_fills = set()
    self._og_ready.set_status(OrderGatewayStatus.INITIALIZING)
    try:
      self._order_formatter = {
          product: get_holder_from_product(product).get_formatter()
          for product in self._og_info.products
      }
    except AttributeError as ex:
      self._order_formatter = {
        product.symbol: SimpleOrderFormatter()
        for product in self._og_info.products
      }

    if (self._market_type == 'Spot' and
        self._exchange == 'Uniswap' and
        self._api_version in ('v3', 'v3-arbitrum')):
      self._og_ready = SpotLPOrderGatewayReady()
    elif self._market_type == 'Spot':
      self._og_ready = SpotOrderGatewayReady()
    elif self._market_type in ('Futures', 'Margin'):
      self._og_ready = PositionOrderGatewayReady()
    else:
      raise ValueError('Unknown market_type')
    self._og_ready.set_status(OrderGatewayStatus.INITIALIZING)

    self._logger = logger or logging.getLogger(__name__)
    self._og_logger = SimpleOrderGatewayLogProvider(gen_og_log_request(config, self))
    self._og_logger.exchange_type = getattr(coin_enums_pb2, self._exchange)
    self._num_print_info = 0
    self._latency_recorder = latency_recorder

  def __str__(self):
    return '%s-OG' % self.exchange

  def renew_order_formatter(self):
    self._order_formatter = {
        product: get_holder_from_product(product).get_formatter()
        for product in self._og_info.products
    }

  def print_og_config(self):
    self.log_json_message([currency.currency for currency in self._og_info.currencies],
                          msg=(self.exchange + ' OG currencies'))
    self.log_json_message([product.symbol for product in self._og_info.products],
                          msg=(self.exchange + ' OG products'))

    og_config_dict = json_format.MessageToDict(self._og_config,
                                               including_default_value_fields=True,
                                               preserving_proto_field_name=True)
    self.log_json_message(og_config_dict, msg=(self.exchange + ' OG config'))

  def get_working_order(self):
    return self._order_manager.get_working_order()

  def get_futures_position(self, product):
    assert product in self._og_info.products, product
    return self._og_info.get_futures_position(product)

  def print_working_orders(self):
    print_working_orders(self._logger, self)

  def print_info(self):
    self._num_print_info += 1
    self.print_working_orders()

    if self._num_print_info == 1:
      self.print_og_config()

    if self.is_ready() and self._fill_checker is not None and self._fill_checker.is_initialized():
      table = self._fill_checker.get_formatted_comparison_table()
      self._logger.info('%s\n', table)

  def check_fill_consistency(self):
    if self._fill_checker is None or not self.is_ready():
      return

    if not self._fill_checker.is_initialized():
      self._fill_checker.reinit()
      return

    if not self._fill_checker.check():
      table = self._fill_checker.get_formatted_comparison_table()
      self._logger.error('Missing fill? \n%s\n', table)
      self._fill_checker.reinit()

  def start_scheduler(self):
    assert isinstance(self._scheduler, Scheduler), self._scheduler
    self._scheduler.add_periodic_task(period=10, job_func=self.print_info)
    if self._og_config.use_fill_checker:
      self._fill_checker = FillChecker(self._og_info, market_type=self._market_type)
      self._scheduler.add_periodic_task(period=1, job_func=self.check_fill_consistency)
    self._scheduler.start()

  def process_fill(self, fill_proto: ProductFillElement, timestamp, order=None):
    if order is None:
      order = self._order_manager.get(exchange_order_id=fill_proto.exchange_order_id)
      if order is None:
        order = self._order_manager.get(order_id=fill_proto.order_id)

    if order is None:
      self._logger.error('Order not found!\n%s', fill_proto)
      return

    self._publish_fill(fill_proto.price, fill_proto.qty, order)
    price_last_fill = order.internal.fill.value.price_last_fill or 0.0
    qty_last_fill = order.internal.fill.value.qty_last_fill or 0.0
    is_close = (math.isclose(price_last_fill, fill_proto.price, abs_tol=1e-10)
                and math.isclose(qty_last_fill, fill_proto.qty, abs_tol=1e-10))

    if not is_close:
      self._logger.warning('order fill info not set! %s, %s', order, fill_proto)
      order.internal.fill.value.price_last_fill = fill_proto.price
      order.internal.fill.value.qty_last_fill = fill_proto.qty

    fill_type = fill_proto.fill_type if fill_proto.HasField('fill_type') else None
    guess_fill_type = fill_proto.guess_fill_type if fill_proto.HasField('guess_fill_type') else None
    self._og_logger.gen_order_filled_event_and_log(order,
                                                   timestamp,
                                                   fill_type=fill_type,
                                                   guess_fill_type=guess_fill_type)
    order.internal.fill.log_last_fill(order, self._logger)
    self._processed_fills.add(fill_proto.fill_id)

    if self._fill_checker is not None:
      self._fill_checker.update_by_fill(fill_proto)

  def log_json_message(self, data, *, msg='', level='INFO'):
    assert isinstance(data, (list, dict)), type(data)
    if isinstance(level, str):
      level = logging.getLevelName(level)
    self._logger.log(level, '%s:\n%s\n', msg, pretty_json(data))

  def get_average_entry_price(self, product):
    return self._og_info.get_average_entry_price(product)

  @functools.lru_cache(maxsize=256)
  def _get_message_handler(self, msg_type):
    for attr in dir(self):
      member = getattr(self, attr)
      if getattr(member, '__coin_og_msg_type__', None) == msg_type:
        return member
    return None

  # noinspection PyUnusedLocal
  def on_private_exchange_msg(self, record, queue_data, topic_data):
    msg = record.data
    assert isinstance(msg, PrivateExchangeMessage), type(msg)
    if self._og_config.log_private_exchange_message:
      msg_str = json.dumps(msg, cls=PrivateExchangeMessageEncoder)
      self._og_logger.write_private_exchange_message(msg_str)

    handler = self._get_message_handler(msg.msg_type)
    if handler is None:
      self._logger.error('Unknown private exchange message type: %s', msg)
    else:
      # noinspection PyBroadException
      try:
        handler(msg)
      except Exception:
        msg_str = json.dumps(msg, cls=PrivateExchangeMessageEncoder)
        self._og_logger.write_private_exchange_message(msg_str)
        self._logger.exception('Exception raised from handler.')

  def _guess_fill_type(self, order, fill_price, fill_timestamp):
    """default guessing logic if exchange does not provide fill type info"""
    if not isinstance(fill_price, (int, float)):
      self._logger.error('Invalid fill_price type: %s, %s', fill_price, order)
      return

    if not isinstance(order.price, (int, float)):
      self._logger.error('Invalid fill_price type: %s, %s', fill_price, order)
      return

    fill_type = coin_order_pb2.TAKER_FILL_TYPE
    if math.isclose(fill_price, order.price):
      # if ack_timestamp not found, using submit timestamp
      ack_timestamp = order.internal.submit_ack_timestamp or order.internal.timestamp or 0
      order_lifetime = (fill_timestamp - ack_timestamp) / 1e9  # Can be negative
      if order_lifetime > self._og_config.maker_time_threshold:
        fill_type = coin_order_pb2.MAKER_FILL_TYPE
    return fill_type

  @abc.abstractmethod
  def _async_submit_order(self, order, post_only):
    raise NotImplementedError()
  
  @abc.abstractmethod
  def _async_liquidity_pool_submit_order(self, order, **kwargs):
    raise NotImplementedError()

  @abc.abstractmethod
  def _async_cancel_order(self, order):
    raise NotImplementedError()

  @abc.abstractmethod
  def _handle_submit_response(self, order, msg):
    raise NotImplementedError()

  @abc.abstractmethod
  def _handle_cancel_response(self, order, msg):
    raise NotImplementedError()

  def _create_order(self, product, price, qty, order_side, order_type, order_id, tag=None):
    timestamp = get_timestamp()
    fill = FillManager(price_orig=price, qty_orig=qty)
    order = Order(order_id=order_id,
                  product=product,
                  price=price,
                  qty=qty,
                  order_side=order_side,
                  order_type=order_type,
                  tag=tag,
                  accepted=False,
                  cancel_sent=False,
                  internal=create_order_internal(timestamp=timestamp, fill=fill))
    self._logger.debug('Order created: %s', order)
    return order

  def liquidity_pool_submit_impl(self,  product,  **kwargs):
    assert isinstance(product, self.OGInfoType.ProductType), type(product)
    assert self.is_ready()

    order_id = self._order_id_generator.gen_order_id()
    order = self._create_order(product=product,
                               price=0.01,
                               qty=0.01,
                               order_side=OrderSide.BUY,
                               order_type=OrderType.LIMIT,
                               order_id=order_id,
                               tag='LP')
    
    self._order_manager.add_order(order)
    context_id = self._latency_recorder.get_current_context_id() if self._latency_recorder is not None else None
    self._ioloop.add_callback(self.liquidity_pool_submit_impl_coro, order=order, context_id=context_id, **kwargs)
  
  @gen.coroutine
  def liquidity_pool_submit_impl_coro(self,  order, context_id,  **kwargs):
    timestamp = get_timestamp()
    response = None
    success = True
    self._og_logger.gen_order_submitted_event_and_log(order, timestamp)
    # noinspection PyBroadException
    try:
      if self._latency_recorder is not None:
        self._latency_recorder.set_time(LifeOfSignal.BEFORE_OG_ORDER_SUBMIT,
                                        account_request=self.og_logger.request,
                                        context_id=context_id,
                                        tag=order.order_id)
      response = yield self._async_liquidity_pool_submit_order(order=order, **kwargs)
      if type(response) is concurrent.futures.Future:
          response = yield response
      if hasattr(response, 'elapsed'):
        lib_elapsed_time = int(response.elapsed.total_seconds() * 1e9)
      elif hasattr(response, 'request_time'):
        lib_elapsed_time = int(response.request_time * 1e9)
      else:
        lib_elapsed_time = 0

      if self._latency_recorder is not None:
        self._latency_recorder.set_time(LifeOfSignal.AFTER_OG_ORDER_SUBMIT,
                                        account_request=self.og_logger.request,
                                        context_id=context_id,
                                        tag=order.order_id)
        extra_info = {
            'type': 'submit',
            'context_id': context_id,
            'order_id': order.order_id,
            'lib_elapsed_time': lib_elapsed_time,
            'rest_lib': self._og_config.rest_lib,
            'time_info': getattr(response, 'time_info', None),
        }
        self._latency_recorder.set_extra_info(extra_info, context_id=context_id)
        self._latency_recorder.end(context_id=context_id)
    except requests.exceptions.ReadTimeout as e:
      self._logger.error('Submit error: read timeout. %s', e)
      self._order_manager.remove_order(order)
      success = False
    except requests.exceptions.ConnectionError as e:
      self._logger.error('Submit error: connection is reset. %s', e)
      self._order_manager.remove_order(order)
      success = False
    except tornado.httpclient.HTTPClientError as e:
      self._logger.error(
          'Submit error: connection is reset. %s\nResponse: %s\nRequest: %s\nHeaders: %s',
          e,
          e.response,
          e.response.request,
          e.response.headers,
      )
      self._logger.error("error response body : " + str(e.response.body))
      self._order_manager.remove_order(order)
      success = False
    except Exception:
      self._logger.exception('Exception raised when submitting order.')
      self._order_manager.remove_order(order)
      success = False

    received_timestamp = get_timestamp()
    if success is False:
      self._og_logger.gen_order_response_and_order_event_and_log(symbol=order.product.symbol,
                                                                 success=False,
                                                                 timestamp=received_timestamp,
                                                                 internal_order_id=order.order_id)
      return

    if response is None:
      self._order_manager.remove_order(order)
      self._logger.warning('response is None!')
      return

    if self._og_config.log_private_exchange_message:
      msg = PrivateExchangeMessage(
          msg_type='submit_response',
          data=response,
          received_timestamp=received_timestamp,
          request_timestamp=timestamp,
          request_params=None,
      )
      msg_str = json.dumps(msg, cls=PrivateExchangeMessageEncoder)
      self._og_logger.write_private_exchange_message(msg_str)

    order_event_type = None
    # noinspection PyBroadException
    try:
      order_event_type = self._handle_submit_response(order, convert_to_requests_response(response))
      assert order_event_type in (None, OrderEvent.ORDER_ACCEPTED, OrderEvent.ORDER_REJECTED,
                                  OrderEvent.ORDER_ERROR), order_event_type
      success = True
    except Exception:
      self._logger.exception('Exception raised from submit response handler.')
      self._order_manager.remove_order(order)
      success = False

    self._og_logger.gen_order_response_and_order_event_and_log(symbol=order.product.symbol,
                                                               success=success,
                                                               timestamp=received_timestamp,
                                                               internal_order_id=order.order_id)
    if order_event_type is not None:
      order.internal.submit_ack_timestamp = received_timestamp
      self._og_logger.gen_order_event_and_log(order, received_timestamp, order_event_type)

    elapsed_time = received_timestamp - timestamp
    self._logger.debug('Order submit elapsed time:  %.3f sec', elapsed_time / 1e9)

  def submit_impl(
      self,
      product,
      price: float,
      qty: float,
      order_side: OrderSide,
      order_type: OrderType = OrderType.LIMIT,  # LIMIT
      order_id: Optional[str] = None,
      tag=None,
      post_only=False,
      leverage=None,
  ):
    assert isinstance(product, self.OGInfoType.ProductType), type(product)
    assert self.is_ready()
    assert price > 0, price
    assert qty > 0, qty

    if order_type != OrderType.LIMIT:
      raise NotImplementedError()

    order_id = order_id or self._order_id_generator.gen_order_id()
    order = self._create_order(product=product,
                               price=price,
                               qty=qty,
                               order_side=order_side,
                               order_type=order_type,
                               order_id=order_id,
                               tag=None)
    order.internal.post_only = post_only
    order.internal.leverage = leverage
    self._order_manager.add_order(order)
    if self._latency_recorder is not None:
      context_id = self._latency_recorder.get_current_context_id()
    else:
      context_id = None
    self._ioloop.add_callback(self._submit_impl, order, post_only, context_id)

  @gen.coroutine
  def _submit_impl(self, order, post_only, context_id):
    timestamp = get_timestamp()
    response = None
    success = True
    self._og_logger.gen_order_submitted_event_and_log(order, timestamp)
    # noinspection PyBroadException
    try:
      if self._latency_recorder is not None:
        self._latency_recorder.set_time(LifeOfSignal.BEFORE_OG_ORDER_SUBMIT,
                                        account_request=self.og_logger.request,
                                        context_id=context_id,
                                        tag=order.order_id)
      response = yield self._async_submit_order(order, post_only)
      if type(response) is concurrent.futures.Future:
          response = yield response
      if hasattr(response, 'elapsed'):
        lib_elapsed_time = int(response.elapsed.total_seconds() * 1e9)
      elif hasattr(response, 'request_time'):
        lib_elapsed_time = int(response.request_time * 1e9)
      else:
        lib_elapsed_time = 0

      if self._latency_recorder is not None:
        self._latency_recorder.set_time(LifeOfSignal.AFTER_OG_ORDER_SUBMIT,
                                        account_request=self.og_logger.request,
                                        context_id=context_id,
                                        tag=order.order_id)
        extra_info = {
            'type': 'submit',
            'context_id': context_id,
            'order_id': order.order_id,
            'lib_elapsed_time': lib_elapsed_time,
            'rest_lib': self._og_config.rest_lib,
            'time_info': getattr(response, 'time_info', None),
        }
        self._latency_recorder.set_extra_info(extra_info, context_id=context_id)
        self._latency_recorder.end(context_id=context_id)
    except requests.exceptions.ReadTimeout as e:
      self._logger.error('Submit error: read timeout. %s', e)
      self._order_manager.remove_order(order)
      success = False
    except requests.exceptions.ConnectionError as e:
      self._logger.error('Submit error: connection is reset. %s', e)
      self._order_manager.remove_order(order)
      success = False
    except tornado.httpclient.HTTPClientError as e:
      self._logger.error(
          'Submit error: connection is reset. %s\nResponse: %s\nRequest: %s\nHeaders: %s',
          e,
          e.response,
          e.response.request,
          e.response.headers,
      )
      self._logger.error("error response body : " + str(e.response.body))
      self._order_manager.remove_order(order)
      success = False
    except Exception:
      self._logger.exception('Exception raised when submitting order.')
      self._order_manager.remove_order(order)
      success = False

    received_timestamp = get_timestamp()
    if success is False:
      self._og_logger.gen_order_response_and_order_event_and_log(symbol=order.product.symbol,
                                                                 success=False,
                                                                 timestamp=received_timestamp,
                                                                 internal_order_id=order.order_id)
      return

    if response is None:
      self._logger.warning('response is None!')
      self._order_manager.remove_order(order)
      return

    if self._og_config.log_private_exchange_message:
      msg = PrivateExchangeMessage(
          msg_type='submit_response',
          data=response,
          received_timestamp=received_timestamp,
          request_timestamp=timestamp,
          request_params=None,
      )
      msg_str = json.dumps(msg, cls=PrivateExchangeMessageEncoder)
      self._og_logger.write_private_exchange_message(msg_str)

    order_event_type = None
    # noinspection PyBroadException
    try:
      order_event_type = self._handle_submit_response(order, convert_to_requests_response(response))
      assert order_event_type in (None, OrderEvent.ORDER_ACCEPTED, OrderEvent.ORDER_REJECTED,
                                  OrderEvent.ORDER_ERROR), order_event_type
      success = True
    except Exception:
      self._logger.exception('Exception raised from submit response handler.')
      self._order_manager.remove_order(order)
      success = False

    self._og_logger.gen_order_response_and_order_event_and_log(symbol=order.product.symbol,
                                                               success=success,
                                                               timestamp=received_timestamp,
                                                               internal_order_id=order.order_id)
    if order_event_type is not None:
      order.internal.submit_ack_timestamp = received_timestamp
      self._og_logger.gen_order_event_and_log(order, received_timestamp, order_event_type)

    elapsed_time = received_timestamp - timestamp
    self._logger.debug('Order submit elapsed time:  %.3f sec', elapsed_time / 1e9)

  def cancel_impl(self, order_id, *, ignore_error=False):
    if not self.is_ready():
      self._logger.error('Cannot cancel order, OG is not ready!')
      return

    timestamp = get_timestamp()
    order = self._order_manager.get(order_id=order_id)
    if order is None:
      self._logger.error('No order_id=%s', order_id)
      return

    if not order.accepted:
      self._logger.error('order_id=%s has not been accepted, %s', order_id, order)
      return

    if order.cancel_sent:
      self._logger.debug('cancel has been sent already %s, %s', order_id, order)
      return

    order.cancel_sent = True
    order.internal.cancel_sent_time = timestamp
    if self._latency_recorder is not None:
      context_id = self._latency_recorder.get_current_context_id()
    else:
      context_id = None
    self._ioloop.add_callback(self._cancel_impl, order, ignore_error, context_id)

  # noinspection PyUnusedLocal
  @gen.coroutine
  def _cancel_impl(self, order, ignore_error, context_id):
    timestamp = get_timestamp()
    self._og_logger.gen_cancel_request_and_order_event_and_log(
        symbol=order.product.symbol,
        internal_order_id=order.order_id,
        timestamp=timestamp,
    )
    response = None
    success = True
    # noinspection PyBroadException
    try:
      if self._latency_recorder is not None:
        self._latency_recorder.set_time(LifeOfSignal.BEFORE_OG_ORDER_CANCEL,
                                        account_request=self.og_logger.request,
                                        context_id=context_id,
                                        tag=order.order_id)

      response = yield self._async_cancel_order(order)
      if hasattr(response, 'elapsed'):
        lib_elapsed_time = int(response.elapsed.total_seconds() * 1e9)
      elif hasattr(response, 'request_time'):
        lib_elapsed_time = int(response.request_time * 1e9)
      else:
        lib_elapsed_time = 0

      if self._latency_recorder is not None:
        self._latency_recorder.set_time(LifeOfSignal.AFTER_OG_ORDER_CANCEL,
                                        account_request=self.og_logger.request,
                                        context_id=context_id,
                                        tag=order.order_id)
        extra_info = {
            'type': 'cancel',
            'context_id': context_id,
            'order_id': order.order_id,
            'lib_elapsed_time': lib_elapsed_time,
            'rest_lib': self._og_config.rest_lib,
            'time_info': getattr(response, 'time_info', None),
        }
        self._latency_recorder.set_extra_info(extra_info, context_id=context_id)
        self._latency_recorder.end(context_id=context_id)
    except requests.exceptions.ReadTimeout as e:
      self._logger.error('Cancel error: read timeout. %s', e)
      order.cancel_sent = False
      success = False
    except requests.exceptions.ConnectionError as e:
      self._logger.error('Cancel error: connection is reset. %s', e)
      order.cancel_sent = False
      success = False
    except tornado.httpclient.HTTPClientError as e:
      self._logger.error(
          'Cancel error: connection is reset. %s\nResponse: %s\nRequest: %s\nHeaders: %s',
          e,
          e.response,
          e.response.request,
          e.response.headers,
      )
      self._logger.error("error response body : " + str(e.response.body))
      self._order_manager.remove_order(order)
      success = False
    except Exception:
      self._logger.exception('Exception raised when requesting cancel.')
      order.cancel_sent = False
      success = False

    if success is False:
      self._og_logger.gen_cancel_response_and_log(symbol=order.product.symbol,
                                                  internal_order_id=order.order_id,
                                                  success=False)
      return

    if response is None:
      return

    received_timestamp = get_timestamp()
    if self._og_config.log_private_exchange_message:
      msg = PrivateExchangeMessage(
          msg_type='cancel_response',
          data=response,
          received_timestamp=received_timestamp,
          request_timestamp=timestamp,
          request_params=None,
      )
      msg_str = json.dumps(msg, cls=PrivateExchangeMessageEncoder)
      self._og_logger.write_private_exchange_message(msg_str)

    order_event_type = None
    # noinspection PyBroadException
    try:
      order_event_type = self._handle_cancel_response(order, convert_to_requests_response(response))
      assert order_event_type in (None, OrderEvent.CANCEL_CONFIRMED, OrderEvent.CANCEL_ACCEPTED,
                                  OrderEvent.CANCEL_ERROR), order_event_type
      success = True
    except Exception:
      self._logger.exception('Exception raised from cancel response handler.')
      order.cancel_sent = False

    self._og_logger.gen_cancel_response_and_log(symbol=order.product.symbol,
                                                internal_order_id=order.order_id,
                                                success=success)

    if order_event_type is not None:
      self._og_logger.gen_order_event_and_log(order, received_timestamp, order_event_type)

    elapsed_time = received_timestamp - timestamp
    self._logger.debug('Order cancel elapsed time:  %.3f sec', elapsed_time / 1e9)

  def cancel_multiple_impl(self, order_ids, *, ignore_error=False):
    for n, order_id in enumerate(order_ids):
      self.cancel(order_id, ignore_error=ignore_error)

  def cancel_all_impl(self, *, ignore_error=False):
    order_ids = [order.order_id for order in self.get_working_order()]
    self.cancel_multiple(order_ids, ignore_error=ignore_error)

  def cancel_product_impl(self, product, *, ignore_error):
    order_ids = [order.order_id for order in self.get_working_order() if order.product == product]
    self.cancel_multiple(order_ids, ignore_error=ignore_error)
